package net.minecraft.block.entity;

import com.mojang.logging.LogUtils;
import com.mojang.serialization.Codec;
import java.util.HashSet;
import java.util.Set;
import net.minecraft.block.BlockState;
import net.minecraft.component.ComponentChanges;
import net.minecraft.component.ComponentMap;
import net.minecraft.component.ComponentType;
import net.minecraft.component.ComponentsAccess;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.MergedComponentMap;
import net.minecraft.inventory.Inventory;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.listener.ClientPlayPacketListener;
import net.minecraft.network.packet.Packet;
import net.minecraft.registry.Registries;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.storage.NbtReadView;
import net.minecraft.storage.NbtWriteView;
import net.minecraft.storage.ReadView;
import net.minecraft.storage.WriteView;
import net.minecraft.text.Text;
import net.minecraft.text.TextCodecs;
import net.minecraft.util.ErrorReporter;
import net.minecraft.util.ItemScatterer;
import net.minecraft.util.crash.CrashCallable;
import net.minecraft.util.crash.CrashReportSection;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.util.math.ChunkSectionPos;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;

/**
 * A block entity is an object holding extra data about a block in a world.
 * Blocks hold their data using pre-defined, finite sets of {@link BlockState};
 * however, some blocks need to hold data that cannot be pre-defined, such as
 * inventories of chests, texts of signs, or pattern combinations of banners.
 * Block entities can hold these data.
 * 
 * <p>Block entities have two other important additions to normal blocks: they
 * can define custom rendering behaviors, and they can tick on every server tick
 * instead of randomly. Some block entities only use these without any extra data.
 * 
 * <p>Block entities are bound to a world and there is one instance of {@link
 * BlockEntity} per the block position, unlike {@link net.minecraft.block.Block}
 * or {@link BlockState} which are reused. Block entities are created using {@link
 * BlockEntityType}, a type of block entities. In most cases, block entities do not
 * have to be constructed manually except in {@link
 * net.minecraft.block.BlockEntityProvider#createBlockEntity}.
 * 
 * <p>To get the block entity at a certain position, use {@link World#getBlockEntity}.
 * Note that the block entity returned can be, in rare cases, different from the
 * one associated with the block at that position. For this reason the return value
 * should not be cast unsafely.
 * 
 * <p>Block entities, like entities, use NBT for the storage of data. The data is
 * loaded to the instance's fields in {@link #readNbt} and written to NBT in
 * {@link #writeNbt}. When a data that needs to be saved has changed, always make sure
 * to call {@link #markDirty()}.
 * 
 * <p>See {@link net.minecraft.block.BlockEntityProvider} and {@link BlockEntityType}
 * for information on creating a block with block entities.
 * 
 * <p>Block entity's data, unlike block states, are not automatically synced. Block
 * entities declare when and which data to sync. In general, block entities need to
 * sync states observable from the clients without specific interaction (such as opening
 * a container). {@link #toUpdatePacket} and {@link #toInitialChunkDataNbt} control
 * which data is sent to the client. To sync the block entity to the client, call
 * {@code serverWorld.getChunkManager().markForUpdate(this.getPos());}.
 */
public abstract class BlockEntity {
	private static final Codec<BlockEntityType<?>> TYPE_CODEC = Registries.BLOCK_ENTITY_TYPE.getCodec();
	private static final Logger LOGGER = LogUtils.getLogger();
	private final BlockEntityType<?> type;
	@Nullable
	protected World world;
	protected final BlockPos pos;
	protected boolean removed;
	private BlockState cachedState;
	private ComponentMap components = ComponentMap.EMPTY;

	public BlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
		this.type = type;
		this.pos = pos.toImmutable();
		this.validateSupports(state);
		this.cachedState = state;
	}

	private void validateSupports(BlockState state) {
		if (!this.supports(state)) {
			throw new IllegalStateException("Invalid block entity " + this.getNameForReport() + " state at " + this.pos + ", got " + state);
		}
	}

	public boolean supports(BlockState state) {
		return this.type.supports(state);
	}

	/**
	 * {@return the block position from {@code nbt}}
	 * 
	 * <p>The passed NBT should use lowercase {@code x}, {@code y}, and {@code z}
	 * keys to store the position. This is incompatible with {@link
	 * net.minecraft.nbt.NbtHelper#fromBlockPos} that use uppercase keys.
	 */
	public static BlockPos posFromNbt(ChunkPos chunkPos, NbtCompound nbt) {
		int i = nbt.getInt("x", 0);
		int j = nbt.getInt("y", 0);
		int k = nbt.getInt("z", 0);
		int l = ChunkSectionPos.getSectionCoord(i);
		int m = ChunkSectionPos.getSectionCoord(k);
		if (l != chunkPos.x || m != chunkPos.z) {
			LOGGER.warn("Block entity {} found in a wrong chunk, expected position from chunk {}", nbt, chunkPos);
			i = chunkPos.getOffsetX(ChunkSectionPos.getLocalCoord(i));
			k = chunkPos.getOffsetZ(ChunkSectionPos.getLocalCoord(k));
		}

		return new BlockPos(i, j, k);
	}

	/**
	 * {@return the world the block entity belongs to}
	 * 
	 * <p>This can return {@code null} during world generation.
	 */
	@Nullable
	public World getWorld() {
		return this.world;
	}

	/**
	 * Sets the world the block entity belongs to.
	 * 
	 * <p>This should not be called manually; however, this can be overridden
	 * to initialize fields dependent on the world.
	 */
	public void setWorld(World world) {
		this.world = world;
	}

	public boolean hasWorld() {
		return this.world != null;
	}

	/**
	 * Reads data from {@code nbt}. Subclasses should override this if they
	 * store a persistent data.
	 * 
	 * <p>NBT is a storage format; therefore, a data from NBT is loaded to a
	 * block entity instance's fields, which are used for other operations instead
	 * of the NBT. The data is written back to NBT when saving the block entity.
	 * 
	 * <p>{@code nbt} might not have all expected keys, or might have a key whose
	 * value does not meet the requirement (such as the type or the range). This
	 * method should fall back to a reasonable default value instead of throwing an
	 * exception.
	 * 
	 * @see #writeNbt
	 */
	protected void readData(ReadView view) {
	}

	public final void read(ReadView view) {
		this.readData(view);
		this.components = (ComponentMap)view.read("components", ComponentMap.CODEC).orElse(ComponentMap.EMPTY);
	}

	public final void readComponentlessData(ReadView view) {
		this.readData(view);
	}

	/**
	 * Writes data to {@code nbt}. Subclasses should override this if they
	 * store a persistent data.
	 * 
	 * <p>NBT is a storage format; therefore, a data from NBT is loaded to a
	 * block entity instance's fields, which are used for other operations instead
	 * of the NBT. The data is written back to NBT when saving the block entity.
	 * 
	 * @see #readNbt
	 */
	protected void writeData(WriteView view) {
	}

	/**
	 * {@return the block entity's NBT data with identifying data}
	 * 
	 * <p>In addition to data written at {@link #writeNbt}, this also
	 * writes the {@linkplain #writeIdToNbt block entity type ID} and the
	 * position of the block entity.
	 * 
	 * @see #createNbt
	 * @see #createNbtWithId
	 */
	public final NbtCompound createNbtWithIdentifyingData(RegistryWrapper.WrapperLookup registries) {
		NbtCompound var4;
		try (ErrorReporter.Logging logging = new ErrorReporter.Logging(this.getReporterContext(), LOGGER)) {
			NbtWriteView nbtWriteView = NbtWriteView.create(logging, registries);
			this.writeFullData(nbtWriteView);
			var4 = nbtWriteView.getNbt();
		}

		return var4;
	}

	public void writeFullData(WriteView view) {
		this.writeDataWithoutId(view);
		this.writeIdentifyingData(view);
	}

	/**
	 * {@return the block entity's NBT data with block entity type ID}
	 * 
	 * <p>In addition to data written at {@link #writeNbt}, this also
	 * writes the {@linkplain #writeIdToNbt block entity type ID}.
	 * 
	 * @see #createNbt
	 * @see #createNbtWithIdentifyingData
	 */
	public void writeDataWithId(WriteView view) {
		this.writeDataWithoutId(view);
		this.writeId(view);
	}

	/**
	 * {@return the block entity's NBT data}
	 * 
	 * <p>Internally, this calls {@link #writeNbt} with a new {@link NbtCompound}
	 * and returns the compound.
	 * 
	 * @see #writeNbt
	 * @see #createNbtWithIdentifyingData
	 * @see #createNbtWithId
	 */
	public final NbtCompound createNbt(RegistryWrapper.WrapperLookup registries) {
		NbtCompound var4;
		try (ErrorReporter.Logging logging = new ErrorReporter.Logging(this.getReporterContext(), LOGGER)) {
			NbtWriteView nbtWriteView = NbtWriteView.create(logging, registries);
			this.writeDataWithoutId(nbtWriteView);
			var4 = nbtWriteView.getNbt();
		}

		return var4;
	}

	public void writeDataWithoutId(WriteView data) {
		this.writeData(data);
		data.put("components", ComponentMap.CODEC, this.components);
	}

	public final NbtCompound createComponentlessNbt(RegistryWrapper.WrapperLookup registries) {
		NbtCompound var4;
		try (ErrorReporter.Logging logging = new ErrorReporter.Logging(this.getReporterContext(), LOGGER)) {
			NbtWriteView nbtWriteView = NbtWriteView.create(logging, registries);
			this.writeComponentlessData(nbtWriteView);
			var4 = nbtWriteView.getNbt();
		}

		return var4;
	}

	public void writeComponentlessData(WriteView view) {
		this.writeData(view);
	}

	/**
	 * Writes the block entity type ID to {@code nbt} under the {@code id} key.
	 * 
	 * @throws RuntimeException if the block entity type is not registered in
	 * the registry
	 */
	private void writeId(WriteView view) {
		writeId(view, this.getType());
	}

	/**
	 * Writes the ID of {@code type} to {@code nbt} under the {@code id} key.
	 */
	public static void writeId(WriteView view, BlockEntityType<?> type) {
		view.put("id", TYPE_CODEC, type);
	}

	/**
	 * Writes to {@code nbt} the block entity type ID under the {@code id} key,
	 * and the block's position under {@code x}, {@code y}, and {@code z} keys.
	 * 
	 * @throws RuntimeException if the block entity type is not registered in
	 * the registry
	 */
	private void writeIdentifyingData(WriteView view) {
		this.writeId(view);
		view.putInt("x", this.pos.getX());
		view.putInt("y", this.pos.getY());
		view.putInt("z", this.pos.getZ());
	}

	/**
	 * {@return the new block entity loaded from {@code nbt}, or {@code null} if it fails}
	 * 
	 * <p>This is used during chunk loading. This can fail if {@code nbt} has an improper or
	 * unregistered {@code id}, or if {@link #readNbt} throws an exception; in these cases,
	 * this logs an error and returns {@code null}.
	 */
	@Nullable
	public static BlockEntity createFromNbt(BlockPos pos, BlockState state, NbtCompound nbt, RegistryWrapper.WrapperLookup registries) {
		BlockEntityType<?> blockEntityType = (BlockEntityType<?>)nbt.get("id", TYPE_CODEC).orElse(null);
		if (blockEntityType == null) {
			LOGGER.error("Skipping block entity with invalid type: {}", nbt.get("id"));
			return null;
		} else {
			BlockEntity blockEntity;
			try {
				blockEntity = blockEntityType.instantiate(pos, state);
			} catch (Throwable var12) {
				LOGGER.error("Failed to create block entity {} for block {} at position {} ", blockEntityType, pos, state, var12);
				return null;
			}

			try {
				BlockEntity var7;
				try (ErrorReporter.Logging logging = new ErrorReporter.Logging(blockEntity.getReporterContext(), LOGGER)) {
					blockEntity.read(NbtReadView.create(logging, registries, nbt));
					var7 = blockEntity;
				}

				return var7;
			} catch (Throwable var11) {
				LOGGER.error("Failed to load data for block entity {} for block {} at position {}", blockEntityType, pos, state, var11);
				return null;
			}
		}
	}

	/**
	 * Marks this block entity as dirty and that it needs to be saved.
	 * This also triggers {@linkplain World#updateComparators comparator update}.
	 * 
	 * <p>This <strong>must be called</strong> when something changed in a way that
	 * affects the saved NBT; otherwise, the game might not save the block entity.
	 */
	public void markDirty() {
		if (this.world != null) {
			markDirty(this.world, this.pos, this.cachedState);
		}
	}

	protected static void markDirty(World world, BlockPos pos, BlockState state) {
		world.markDirty(pos);
		if (!state.isAir()) {
			world.updateComparators(pos, state.getBlock());
		}
	}

	/**
	 * {@return the block entity's position}
	 */
	public BlockPos getPos() {
		return this.pos;
	}

	/**
	 * {@return the cached block state at the block entity's position}
	 * 
	 * <p>This is faster than calling {@link World#getBlockState}.
	 */
	public BlockState getCachedState() {
		return this.cachedState;
	}

	/**
	 * {@return the packet to send to nearby players when the block entity's observable
	 * state changes, or {@code null} to not send the packet}
	 * 
	 * <p>If the data returned by {@link #toInitialChunkDataNbt initial chunk data} is suitable
	 * for updates, the following shortcut can be used to create an update packet: {@code
	 * BlockEntityUpdateS2CPacket.create(this)}. The NBT will be passed to {@link #readNbt}
	 * on the client.
	 * 
	 * <p>"Observable state" is a state that clients can observe without specific interaction.
	 * For example, {@link CampfireBlockEntity}'s cooked items are observable states,
	 * but chests' inventories are not observable states, since the player must first open
	 * that chest before they can see the contents.
	 * 
	 * <p>To sync block entity data using this method, use {@code
	 * serverWorld.getChunkManager().markForUpdate(this.getPos());}.
	 * 
	 * @see #toInitialChunkDataNbt
	 */
	@Nullable
	public Packet<ClientPlayPacketListener> toUpdatePacket() {
		return null;
	}

	/**
	 * {@return the serialized state of this block entity that is observable by clients}
	 * 
	 * <p>This is sent alongside the initial chunk data, as well as when the block
	 * entity implements {@link #toUpdatePacket} and decides to use the default
	 * {@link net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket}.
	 * 
	 * <p>"Observable state" is a state that clients can observe without specific interaction.
	 * For example, {@link CampfireBlockEntity}'s cooked items are observable states,
	 * but chests' inventories are not observable states, since the player must first open
	 * that chest before they can see the contents.
	 * 
	 * <p>To send all NBT data of this block entity saved to disk, return {@link #createNbt}.
	 * 
	 * @see #toUpdatePacket
	 */
	public NbtCompound toInitialChunkDataNbt(RegistryWrapper.WrapperLookup registries) {
		return new NbtCompound();
	}

	public boolean isRemoved() {
		return this.removed;
	}

	public void markRemoved() {
		this.removed = true;
	}

	public void cancelRemoval() {
		this.removed = false;
	}

	public void onBlockReplaced(BlockPos pos, BlockState oldState) {
		if (this instanceof Inventory inventory && this.world != null) {
			ItemScatterer.spawn(this.world, pos, inventory);
		}
	}

	/**
	 * If this block entity's block extends {@link net.minecraft.block.BlockWithEntity},
	 * this is called inside {@link net.minecraft.block.AbstractBlock#onSyncedBlockEvent}.
	 * 
	 * @see net.minecraft.block.AbstractBlock#onSyncedBlockEvent
	 */
	public boolean onSyncedBlockEvent(int type, int data) {
		return false;
	}

	public void populateCrashReport(CrashReportSection crashReportSection) {
		crashReportSection.add("Name", this::getNameForReport);
		crashReportSection.add("Cached block", this.getCachedState()::toString);
		if (this.world == null) {
			crashReportSection.add("Block location", (CrashCallable<String>)(() -> this.pos + " (world missing)"));
		} else {
			crashReportSection.add("Actual block", this.world.getBlockState(this.pos)::toString);
			CrashReportSection.addBlockLocation(crashReportSection, this.world, this.pos);
		}
	}

	public String getNameForReport() {
		return Registries.BLOCK_ENTITY_TYPE.getId(this.getType()) + " // " + this.getClass().getCanonicalName();
	}

	public BlockEntityType<?> getType() {
		return this.type;
	}

	@Deprecated
	public void setCachedState(BlockState state) {
		this.validateSupports(state);
		this.cachedState = state;
	}

	protected void readComponents(ComponentsAccess components) {
	}

	public final void readComponents(ItemStack stack) {
		this.readComponents(stack.getDefaultComponents(), stack.getComponentChanges());
	}

	public final void readComponents(ComponentMap defaultComponents, ComponentChanges components) {
		final Set<ComponentType<?>> set = new HashSet();
		set.add(DataComponentTypes.BLOCK_ENTITY_DATA);
		set.add(DataComponentTypes.BLOCK_STATE);
		final ComponentMap componentMap = MergedComponentMap.create(defaultComponents, components);
		this.readComponents(new ComponentsAccess() {
			@Nullable
			@Override
			public <T> T get(ComponentType<? extends T> type) {
				set.add(type);
				return componentMap.get(type);
			}

			@Override
			public <T> T getOrDefault(ComponentType<? extends T> type, T fallback) {
				set.add(type);
				return componentMap.getOrDefault(type, fallback);
			}
		});
		ComponentChanges componentChanges = components.withRemovedIf(set::contains);
		this.components = componentChanges.toAddedRemovedPair().added();
	}

	protected void addComponents(ComponentMap.Builder builder) {
	}

	@Deprecated
	public void removeFromCopiedStackData(WriteView view) {
	}

	public final ComponentMap createComponentMap() {
		ComponentMap.Builder builder = ComponentMap.builder();
		builder.addAll(this.components);
		this.addComponents(builder);
		return builder.build();
	}

	public ComponentMap getComponents() {
		return this.components;
	}

	public void setComponents(ComponentMap components) {
		this.components = components;
	}

	@Nullable
	public static Text tryParseCustomName(ReadView view, String key) {
		return (Text)view.read(key, TextCodecs.CODEC).orElse(null);
	}

	public ErrorReporter.Context getReporterContext() {
		return new BlockEntity.ReporterContext(this);
	}

	record ReporterContext(BlockEntity blockEntity) implements ErrorReporter.Context {
		@Override
		public String getName() {
			return this.blockEntity.getNameForReport() + "@" + this.blockEntity.getPos();
		}
	}
}
